package com.demo.stock.service.impl;

import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.demo.stock.enums.ReduceStockTypeEnum;
import com.demo.stock.init.InitLuaScript;
import com.demo.stock.redis.RedisService;
import com.demo.stock.service.AbstractStockService;
import com.demo.stock.service.ReduceStockService;
import com.demo.stock.utils.SpringContextUtils;
import com.demo.stock.vo.BatchReduceStockVo;
import com.demo.stock.vo.StockVo;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * 批量扣除库存Service类
 *
 * @author admin
 */
@Service("reduceStockService")
public class ReduceStockServiceImpl implements ReduceStockService {
    private static final Logger log = LoggerFactory.getLogger(ReduceStockServiceImpl.class);

    @Resource
    private InitLuaScript initLuaScript;

    @Resource
    private RedisService redisService;

    /**
     * 批量更新商品库存
     *
     * @param map                 需要更新的库存列表，key为商品编码，value为扣减的数量
     * @param reduceStockTypeEnum 扣除库存的方式
     * @param mysqlBeanName       mysql 扣库的bean
     * @param redisKeyPrefix      redis key 前缀
     *
     * @return BatchDeductionVo
     */
    @Override
    public BatchReduceStockVo batchReduceStock(Map<String, Integer> map, ReduceStockTypeEnum reduceStockTypeEnum,
                                               String mysqlBeanName, String redisKeyPrefix) {
        boolean redisFlag = false;
        BatchReduceStockVo batchReduceStockVo = new BatchReduceStockVo();
        try {
            if (map.isEmpty()) {
                log.error("batchDeductionStockByList map is null");
                batchReduceStockVo.setResult(false);
                batchReduceStockVo.setMessage("请求参数为空");
                return batchReduceStockVo;
            }
            Map<String, Integer> productInfoMap = new HashMap<>(16);
            // 入参检测，判断数量是否有小于等于0的情况
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                Integer num = entry.getValue();
                if (num <= 0) {
                    log.error("batchDeductionStockByList error. num <= 0. map:{}", map);
                    batchReduceStockVo.setResult(false);
                    batchReduceStockVo.setMessage("存在扣库数量小于等于0的情况");
                    batchReduceStockVo.setErrorStockVoList(toStockList(map));
                    batchReduceStockVo.setSuccessStockVoList(new ArrayList<>());
                    return batchReduceStockVo;
                }
                // 重定义商品的key, key = 前缀 + 商品编码
                String key = redisKeyPrefix + entry.getKey();
                productInfoMap.put(key, num);
            }
            // 扣除库存的方式
            String deductionStockType = reduceStockTypeEnum.getType();
            // 扣除多个商品库存时，某一个商品库存不足，其他的全部也不扣除
            if (ReduceStockTypeEnum.NONE.getType().equals(deductionStockType)) {
                batchReduceStockVo = deductionStockWithNone(redisKeyPrefix, productInfoMap, deductionStockType);
            } else if (ReduceStockTypeEnum.SKIP.getType().equals(deductionStockType)
                    || ReduceStockTypeEnum.ALL.getType().equals(deductionStockType)) {
                // 扣除多个商品库存时，某一个商品库存不足，跳过该商品，不影响其他券的库存扣减
                batchReduceStockVo = deductionStockWithSkip(redisKeyPrefix, productInfoMap, deductionStockType);
            }
            redisFlag = true;
            // 返回结果不为空时，批量扣除mysql中商品的库存
            if (batchReduceStockVo.getResult()) {
                List<StockVo> stockVoList = batchReduceStockVo.getSuccessStockVoList();
                if (CollectionUtils.isNotEmpty(stockVoList)) {
                    log.info("productList is:{}", stockVoList);
                    // mysql数据的回滚由事务处理， redis数据手动回滚
                    boolean dataFlag = reduceDataStock(mysqlBeanName, stockVoList);
                    if (!dataFlag) {
                        batchReduceStockVo.setMessage("数据库扣库失败");
                        // 回滚数据
                        log.info("数据库扣库失败");
                        rollback(redisKeyPrefix, map, true, batchReduceStockVo, null);
                        return batchReduceStockVo;
                    }
                }
            } else {
                // 失败时，返回成功的list为空
                batchReduceStockVo.setResult(false);
                batchReduceStockVo.setSuccessStockVoList(new ArrayList<>());
                batchReduceStockVo.setErrorStockVoList(toStockList(map));
            }
        } catch (Exception e) {
            rollback(redisKeyPrefix, map, redisFlag, batchReduceStockVo, e);
        }
        return batchReduceStockVo;
    }

    /**
     * 异常回滚
     *
     * @param redisKeyPrefix     redis key前缀
     * @param map                要扣库的数量信息
     * @param redisFlag          redis 扣库是否已经成功
     * @param batchReduceStockVo 批量扣库请求参数
     * @param e                  异常信息
     */
    private void rollback(String redisKeyPrefix, Map<String, Integer> map, boolean redisFlag, BatchReduceStockVo batchReduceStockVo, Exception e) {
        log.error("batchDeductionStockByList error. map:{}", map, e);
        // 异常时，回滚数据，保证数据的最终一致性
        boolean flag = batchReduceStockVo.getResult();
        List<StockVo> successStockVoList = batchReduceStockVo.getSuccessStockVoList();
        // 如果redis有操作成功，回滚redis的数据
        if (flag && CollectionUtils.isNotEmpty(successStockVoList) && redisFlag) {
            rollbackRedisStock(redisKeyPrefix, successStockVoList);
        }
        batchReduceStockVo.setResult(false);
        List<StockVo> errorStockVoList = batchReduceStockVo.getErrorStockVoList();
        if (CollectionUtils.isEmpty(errorStockVoList)) {
            errorStockVoList = new ArrayList<>();
        }

        List<StockVo> originList = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(successStockVoList)) {
            for (StockVo stockVo : successStockVoList) {
                StockVo newStockVo = new StockVo();
                newStockVo.setUuid(stockVo.getUuid());
                newStockVo.setNum(map.get(stockVo.getUuid()));
                originList.add(newStockVo);
            }
            errorStockVoList.addAll(originList);
        }

        batchReduceStockVo.setSuccessStockVoList(new ArrayList<>());
        batchReduceStockVo.setErrorStockVoList(errorStockVoList);
    }


    /**
     * 数据库库存扣减
     *
     * @param mysqlBeanName mysql扣库bean
     * @param stockVoList   商品扣库信息
     *
     * @return 数据库扣库结果
     */
    private boolean reduceDataStock(String mysqlBeanName, List<StockVo> stockVoList) {
        if (StringUtils.isBlank(mysqlBeanName)) {
            return true;
        }
        try {
            AbstractStockService abstractStockService = SpringContextUtils.getBean(mysqlBeanName);
            return abstractStockService.batchReduceStock(stockVoList);
        } catch (Exception e) {
            log.info("mysql reduceStockFailed. beanName:{}, stockVoList:{}", mysqlBeanName, stockVoList, e);
            return false;
        }
    }

    /**
     * 扣除多个商品库存时，某一个商品库存不足，跳过该商品，不影响其他券的库存扣减
     *
     * @param redisKeyPrefix     redis key 前缀
     * @param map                待扣除库存的商品列表
     * @param deductionStockType 扣库方式
     *
     * @return BatchDeductionVo
     */
    private BatchReduceStockVo deductionStockWithSkip(String redisKeyPrefix, Map<String, Integer> map,
                                                      String deductionStockType) {

        // 先将要扣库的数据存到redis, 后续直接操作redis执行遍历
        String uuid = getUuid();
        redisService.hmset(uuid, map, -1);
        // lua脚本入参：key
        List<String> keyList = new ArrayList<>();
        keyList.add(uuid);
        keyList.add(deductionStockType);
        String luaScript = initLuaScript.getLuaScript("deductionStock");
        String result = redisService.eval(luaScript, keyList, new ArrayList<>());
        // 解析结果,如果有扣除失败的结果(不处理，失败的已经自动跳过)
        BatchReduceStockVo newBatchReduceStockVo = parseLuaResult(redisKeyPrefix, result);
        if (CollectionUtils.isNotEmpty(newBatchReduceStockVo.getSuccessStockVoList())) {
            newBatchReduceStockVo.setResult(true);
        } else {
            newBatchReduceStockVo.setResult(false);
        }
        redisService.del(uuid);
        return newBatchReduceStockVo;

    }

    /**
     * 扣除多个商品库存时，某一个商品库存不足，其他的全部也不扣除
     *
     * @param redisKeyPrefix     redis key 前缀
     * @param map                待扣除库存的商品列表
     * @param deductionStockType 扣库方式
     *
     * @return BatchDeductionVo
     */
    private BatchReduceStockVo deductionStockWithNone(String redisKeyPrefix, Map<String, Integer> map,
                                                      String deductionStockType) {
        BatchReduceStockVo batchReduceStockVo = new BatchReduceStockVo();

        // 先将要扣库的数据存到redis, 后续直接操作redis执行遍历
        String uuid = getUuid();
        redisService.hmset(uuid, map, -1);
        // lua脚本入参：key
        List<String> keyList = new ArrayList<>();
        keyList.add(uuid);
        keyList.add(deductionStockType);
        String luaScript = initLuaScript.getLuaScript("deductionStock");
        String result = redisService.eval(luaScript, keyList, new ArrayList<>());
        // 解析结果
        BatchReduceStockVo newBatchReduceStockVo = parseLuaResult(redisKeyPrefix, result);
        // 如果有扣除失败的结果，回滚全部数据
        List<StockVo> errorStockVoList = newBatchReduceStockVo.getErrorStockVoList();
        if (CollectionUtils.isNotEmpty(errorStockVoList)) {
            // 回滚数据
            rollbackRedisStock(redisKeyPrefix, newBatchReduceStockVo.getSuccessStockVoList());
            // 设置返回结果为失败
            batchReduceStockVo.setResult(false);
            batchReduceStockVo.setErrorStockVoList(errorStockVoList);
            batchReduceStockVo.setSuccessStockVoList(newBatchReduceStockVo.getSuccessStockVoList());
        } else {
            // 设置返回结果为成功
            batchReduceStockVo.setResult(true);
            batchReduceStockVo.setSuccessStockVoList(newBatchReduceStockVo.getSuccessStockVoList());
        }
        redisService.del(uuid);
        return batchReduceStockVo;

    }

    /**
     * 回滚商品的库存信息
     *
     * @param redisKeyPrefix redis key 前缀
     * @param stockVoList    商品数据
     */
    private void rollbackRedisStock(String redisKeyPrefix, List<StockVo> stockVoList) {
        if (CollectionUtils.isEmpty(stockVoList)) {
            return;
        }
        // 回滚数据
        Map<String, Integer> rollbackStockMap = new HashMap<>(16);
        for (StockVo stockVo : stockVoList) {
            String newRedisKey = redisKeyPrefix + stockVo.getUuid();
            rollbackStockMap.put(newRedisKey, stockVo.getNum());
        }
        String rollbackUuid = getUuid();
        redisService.hmset(rollbackUuid, rollbackStockMap, -1);
        // 利用lua脚本回滚数据
        List<String> rollbackKeyList = new ArrayList<>();
        rollbackKeyList.add(rollbackUuid);
        String rollbackLuaScript = initLuaScript.getLuaScript("deductionStockRollback");
        redisService.eval(rollbackLuaScript, rollbackKeyList, new ArrayList<>());
        // 删除redis数据
        redisService.del(rollbackUuid);
    }

    /**
     * 解析lua返回的结果, 返回结果格式为： abc,1;bcd,2;|edf:2;
     *
     * @param redisKeyPrefix redis key 前缀
     * @param result         执行lua脚本返回结果
     *
     * @return BatchDeductionVo
     */
    private BatchReduceStockVo parseLuaResult(String redisKeyPrefix, String result) {
        BatchReduceStockVo batchReduceStockVo = new BatchReduceStockVo();
        if (StringUtils.isBlank(result)) {
            return batchReduceStockVo;
        }
        String[] resultArray = result.split("\\|");
        String successStockVo;
        String errorStockVo = "";
        if (resultArray.length == 1) {
            successStockVo = resultArray[0];
        } else {
            successStockVo = resultArray[0];
            errorStockVo = resultArray[1];
        }
        // 扣除库存成功的数据
        List<StockVo> successStockVoList = getStockVoListByString(redisKeyPrefix, successStockVo);
        // 扣除库存失败的数据
        List<StockVo> errorStockVoList = getStockVoListByString(redisKeyPrefix, errorStockVo);
        batchReduceStockVo.setSuccessStockVoList(successStockVoList);
        batchReduceStockVo.setErrorStockVoList(errorStockVoList);
        return batchReduceStockVo;
    }


    /**
     * 获取商品列表信息
     *
     * @param redisKeyPrefix redisKey前缀
     * @param stockInfo      商品信息,包含编码和数量， 格式为：abc,1;bcd,2;前面为编码，后面为数量
     *
     * @return List<StockVo>
     */
    private List<StockVo> getStockVoListByString(String redisKeyPrefix, String stockInfo) {
        List<StockVo> stockVoList = new ArrayList<>();
        if (StringUtils.isNotBlank(stockInfo)) {
            String[] stockVoArray = stockInfo.split(";");
            for (String s : stockVoArray) {
                String[] stockVoInfoArray = s.split(",");
                StockVo stockVo = new StockVo();
                String uuid = stockVoInfoArray[0];
                if (StringUtils.isNotBlank(redisKeyPrefix)) {
                    uuid = uuid.replace(redisKeyPrefix, "");
                }
                stockVo.setUuid(uuid);
                stockVo.setNum(Integer.parseInt(stockVoInfoArray[1]));
                stockVoList.add(stockVo);
            }
        }
        return stockVoList;
    }


    /**
     * 封装结果
     */
    private List<StockVo> toStockList(Map<String, Integer> map) {
        List<StockVo> result = new ArrayList<>();
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            StockVo stockVo = new StockVo();
            stockVo.setUuid(entry.getKey());
            stockVo.setNum(entry.getValue());
            result.add(stockVo);
        }
        return result;
    }


    private String getUuid() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}
